using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using HamsterDocs.Domains.Users;
using HamsterDocs.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.IdentityModel.Tokens;

namespace HamsterDocs.Hosting.Services;

public class AuthService
{
    [HttpPost("login")]
    [EnableRateLimiting("auth")]
    public ActionResult<LoginResponse> Login([FromBody] LoginRequest req,
        [FromServices] IMemoryCache cache,
        [FromServices] IConfiguration configuration,
        [FromServices] IContext context)
    {
        // 校验验证码
        if (string.IsNullOrWhiteSpace(req.CaptchaId) || string.IsNullOrWhiteSpace(req.CaptchaCode))
        {
            return new BadRequestObjectResult(new { error = "CaptchaRequired" });
        }
        var cacheKey = $"captcha:{req.CaptchaId}";
        if (!cache.TryGetValue<int>(cacheKey, out var answer))
        {
            return new BadRequestObjectResult(new { error = "CaptchaExpired" });
        }
        cache.Remove(cacheKey);
        if (!int.TryParse(req.CaptchaCode, out var code) || code != answer)
        {
            return new BadRequestObjectResult(new { error = "CaptchaInvalid" });
        }

        var db = (DbContext)context;
        // 支持用户名或邮箱登录
        var user = db.Set<User>()
            .AsNoTracking()
            .FirstOrDefault(u =>
                (u.Email != null && u.Email == req.Login) ||
                (u.UserName != null && u.UserName == req.Login));

        if (user == null)
        {
            return new BadRequestObjectResult(new { error = "UserNotFound" });
        }
        if (!user.IsActive)
        {
            return new BadRequestObjectResult(new { error = "UserDisabled" });
        }
        if (string.IsNullOrEmpty(user.PasswordSalt) || string.IsNullOrEmpty(user.PasswordHash))
        {
            return new BadRequestObjectResult(new { error = "PasswordNotSet" });
        }

        var computed = HashPassword(req.Password ?? string.Empty, user.PasswordSalt!);
        if (!SlowEquals(computed, user.PasswordHash!))
        {
            return new BadRequestObjectResult(new { error = "PasswordInvalid" });
        }

        // 签发 JWT
        var jwtSection = configuration.GetSection("Jwt");
        var issuer = jwtSection["Issuer"] ?? "hamsterdocs";
        var audience = jwtSection["Audience"] ?? "hamsterdocs-web";
        var expireMinutes = int.TryParse(jwtSection["ExpireMinutes"], out var em) ? em : 120;
        var secret = jwtSection["Secret"];
        if (string.IsNullOrWhiteSpace(secret))
        {
            secret = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
        }
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var claims = new List<Claim>
        {
            new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
            new Claim("role", user.Role ?? "User"),
            new Claim("displayName", user.UserName ?? user.Email ?? string.Empty),
            new Claim("avatar", user.Avatar ?? string.Empty)
        };

        var token = new JwtSecurityToken(
            issuer: issuer,
            audience: audience,
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(expireMinutes),
            signingCredentials: creds);

        var tokenString = new JwtSecurityTokenHandler().WriteToken(token);

        var defaultRoute = (user.Role ?? "User") == "Admin" ? "/admin" : "/panel";

        return new OkObjectResult(new LoginResponse(
            tokenString,
            expireMinutes * 60,
            new LoginUser(user.Id, user.UserName ?? string.Empty, user.Email ?? string.Empty, user.Role ?? "User", user.Avatar ?? string.Empty, defaultRoute)
        ));
    }

    [Authorize]
    [HttpGet("me")]
    public ActionResult<MeResponse> Me(
        [FromServices] IContext context,
        ClaimsPrincipal principal)
    {
        var sub = principal.FindFirst("sub")?.Value;
        if (string.IsNullOrWhiteSpace(sub))
        {
            return new UnauthorizedResult();
        }
        if (!Guid.TryParse(sub, out var userId))
        {
            return new UnauthorizedResult();
        }
        var db = (DbContext)context;
        var user = db.Set<User>()
            .AsNoTracking()
            .FirstOrDefault(u => u.Id == userId);
        if (user == null)
        {
            return new UnauthorizedResult();
        }
        var defaultRoute = (user.Role ?? "User") == "Admin" ? "/admin" : "/panel";
        return new OkObjectResult(new MeResponse(
            user.Id,
            user.UserName ?? string.Empty,
            user.Email ?? string.Empty,
            user.Role ?? "User",
            user.Avatar ?? string.Empty,
            defaultRoute
        ));
    }

    private static string HashPassword(string password, string salt, int iterations = 100000, int length = 32)
    {
        var saltBytes = Convert.FromBase64String(salt);
        using var pbkdf2 = new Rfc2898DeriveBytes(password, saltBytes, iterations, HashAlgorithmName.SHA256);
        var bytes = pbkdf2.GetBytes(length);
        return Convert.ToBase64String(bytes);
    }

    private static string CreateSalt(int length = 16)
    {
        var bytes = new byte[length];
        RandomNumberGenerator.Fill(bytes);
        return Convert.ToBase64String(bytes);
    }

    private static bool SlowEquals(string a, string b)
    {
        var ba = Convert.FromBase64String(a);
        var bb = Convert.FromBase64String(b);
        if (ba.Length != bb.Length) return false;
        int diff = 0;
        for (int i = 0; i < ba.Length; i++)
        {
            diff |= ba[i] ^ bb[i];
        }
        return diff == 0;
    }

    [AllowAnonymous]
    [HttpGet("oauth/providers")]
    public ActionResult<object> GetProviders([FromServices] IConfiguration configuration)
    {
        var gh = configuration.GetSection("OAuth").GetSection("GitHub");
        var ge = configuration.GetSection("OAuth").GetSection("Gitee");
        return new OkObjectResult(new
        {
            github = !string.IsNullOrWhiteSpace(gh["ClientId"]),
            gitee = !string.IsNullOrWhiteSpace(ge["ClientId"])
        });
    }

    [AllowAnonymous]
    [HttpGet("oauth/github/start")]
    public ActionResult<object> GithubStart([FromServices] IConfiguration configuration)
    {
        var gh = configuration.GetSection("OAuth").GetSection("GitHub");
        var clientId = gh["ClientId"];
        var callback = gh["CallbackUrl"];
        if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(callback))
        {
            return new BadRequestObjectResult(new { error = "GitHubNotConfigured" });
        }
        var state = Guid.NewGuid().ToString("N");
        var scope = "read:user user:email";
        var url = $"https://github.com/login/oauth/authorize?client_id={Uri.EscapeDataString(clientId)}&redirect_uri={Uri.EscapeDataString(callback)}&scope={Uri.EscapeDataString(scope)}&state={state}";
        return new OkObjectResult(new { url, state });
    }

    [AllowAnonymous]
    [HttpPost("oauth/github/exchange")]
    public async Task<ActionResult<LoginResponse>> GithubExchange(
        [FromBody] OAuthExchangeRequest req,
        [FromServices] IConfiguration configuration,
        [FromServices] IContext context,
        [FromServices] IHttpClientFactory httpClientFactory)
    {
        var gh = configuration.GetSection("OAuth").GetSection("GitHub");
        var clientId = gh["ClientId"];
        var clientSecret = gh["ClientSecret"];
        if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientSecret))
        {
            return new BadRequestObjectResult(new { error = "GitHubNotConfigured" });
        }

        var client = httpClientFactory.CreateClient();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        client.DefaultRequestHeaders.UserAgent.ParseAdd("HamsterDocs/1.0");

        // Exchange code for access_token
        var tokenReq = new HttpRequestMessage(HttpMethod.Post, "https://github.com/login/oauth/access_token")
        {
            Content = new FormUrlEncodedContent(new Dictionary<string, string>
            {
                ["client_id"] = clientId!,
                ["client_secret"] = clientSecret!,
                ["code"] = req.Code,
                ["redirect_uri"] = req.RedirectUri
            })
        };

        var tokenResp = await client.SendAsync(tokenReq);
        if (!tokenResp.IsSuccessStatusCode)
        {
            return new BadRequestObjectResult(new { error = "OAuthTokenError", status = (int)tokenResp.StatusCode });
        }
        var tokenJson = await tokenResp.Content.ReadAsStringAsync();
        using var tokenDoc = JsonDocument.Parse(tokenJson);
        var accessToken = tokenDoc.RootElement.GetProperty("access_token").GetString();
        if (string.IsNullOrWhiteSpace(accessToken))
        {
            return new BadRequestObjectResult(new { error = "OAuthTokenMissing" });
        }

        // Fetch user info
        var userReq = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/user");
        userReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        var userResp = await client.SendAsync(userReq);
        if (!userResp.IsSuccessStatusCode)
        {
            return new BadRequestObjectResult(new { error = "OAuthUserError", status = (int)userResp.StatusCode });
        }

        var userJson = await userResp.Content.ReadAsStringAsync();
        using var userDoc = JsonDocument.Parse(userJson);
        var providerKey = userDoc.RootElement.GetProperty("id").GetRawText(); // numeric id, keep as string
        var login = userDoc.RootElement.TryGetProperty("login", out var loginProp) ? loginProp.GetString() : null;
        var avatarUrl = userDoc.RootElement.TryGetProperty("avatar_url", out var avProp) ? avProp.GetString() : null;

        return await SignInWithExternalAsync("github", providerKey!, login, null, avatarUrl, configuration, context);
    }

    [AllowAnonymous]
    [HttpGet("oauth/gitee/start")]
    public ActionResult<object> GiteeStart([FromServices] IConfiguration configuration)
    {
        var ge = configuration.GetSection("OAuth").GetSection("Gitee");
        var clientId = ge["ClientId"];
        var callback = ge["CallbackUrl"];
        if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(callback))
        {
            return new BadRequestObjectResult(new { error = "GiteeNotConfigured" });
        }
        var state = Guid.NewGuid().ToString("N");
        var url = $"https://gitee.com/oauth/authorize?client_id={Uri.EscapeDataString(clientId)}&redirect_uri={Uri.EscapeDataString(callback)}&response_type=code&state={state}";
        return new OkObjectResult(new { url, state });
    }

    [AllowAnonymous]
    [HttpPost("oauth/gitee/exchange")]
    public async Task<ActionResult<LoginResponse>> GiteeExchange(
        [FromBody] OAuthExchangeRequest req,
        [FromServices] IConfiguration configuration,
        [FromServices] IContext context,
        [FromServices] IHttpClientFactory httpClientFactory)
    {
        var ge = configuration.GetSection("OAuth").GetSection("Gitee");
        var clientId = ge["ClientId"];
        var clientSecret = ge["ClientSecret"];
        if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientSecret))
        {
            return new BadRequestObjectResult(new { error = "GiteeNotConfigured" });
        }

        var client = httpClientFactory.CreateClient();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        var tokenReq = new HttpRequestMessage(HttpMethod.Post, "https://gitee.com/oauth/token")
        {
            Content = new FormUrlEncodedContent(new Dictionary<string, string>
            {
                ["grant_type"] = "authorization_code",
                ["code"] = req.Code,
                ["client_id"] = clientId!,
                ["client_secret"] = clientSecret!,
                ["redirect_uri"] = req.RedirectUri
            })
        };

        var tokenResp = await client.SendAsync(tokenReq);
        if (!tokenResp.IsSuccessStatusCode)
        {
            return new BadRequestObjectResult(new { error = "OAuthTokenError", status = (int)tokenResp.StatusCode });
        }
        var tokenJson = await tokenResp.Content.ReadAsStringAsync();
        using var tokenDoc = JsonDocument.Parse(tokenJson);
        var accessToken = tokenDoc.RootElement.GetProperty("access_token").GetString();
        if (string.IsNullOrWhiteSpace(accessToken))
        {
            return new BadRequestObjectResult(new { error = "OAuthTokenMissing" });
        }

        var userReq = new HttpRequestMessage(HttpMethod.Get, "https://gitee.com/api/v5/user");
        userReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        var userResp = await client.SendAsync(userReq);
        if (!userResp.IsSuccessStatusCode)
        {
            return new BadRequestObjectResult(new { error = "OAuthUserError", status = (int)userResp.StatusCode });
        }

        var userJson = await userResp.Content.ReadAsStringAsync();
        using var userDoc = JsonDocument.Parse(userJson);
        var providerKey = userDoc.RootElement.GetProperty("id").GetRawText();
        var name = userDoc.RootElement.TryGetProperty("name", out var nameProp) ? nameProp.GetString() : null;
        var avatarUrl = userDoc.RootElement.TryGetProperty("avatar_url", out var avProp) ? avProp.GetString() : null;

        return await SignInWithExternalAsync("gitee", providerKey!, name, null, avatarUrl, configuration, context);
    }

    private async Task<ActionResult<LoginResponse>> SignInWithExternalAsync(
        string provider,
        string providerKey,
        string? userName,
        string? email,
        string? avatarUrl,
        IConfiguration configuration,
        IContext context)
    {
        var db = (DbContext)context;

        // Find existing binding
        var binding = await db.Set<ExternalLogin>()
            .AsNoTracking()
            .FirstOrDefaultAsync(x => x.Provider == provider && x.ProviderKey == providerKey);

        User user;
        if (binding != null)
        {
            user = await db.Set<User>().FirstOrDefaultAsync(u => u.Id == binding.UserId);
            if (user == null)
            {
                return new BadRequestObjectResult(new { error = "BindingUserMissing" });
            }
        }
        else
        {
            // Create new user
            user = new User
            {
                UserName = userName ?? $"{provider}_{providerKey}",
                Email = email,
                Avatar = avatarUrl,
                Role = "User",
                IsActive = true,
                CreatedAt = DateTime.UtcNow,
                UpdatedAt = DateTime.UtcNow
            };
            db.Set<User>().Add(user);
            await db.SaveChangesAsync();

            var ext = new ExternalLogin
            {
                Provider = provider,
                ProviderKey = providerKey,
                UserId = user.Id,
                CreatedAt = DateTime.UtcNow
            };
            db.Set<ExternalLogin>().Add(ext);
            await db.SaveChangesAsync();
        }

        // Issue JWT
        var jwtSection = configuration.GetSection("Jwt");
        var issuer = jwtSection["Issuer"] ?? "hamsterdocs";
        var audience = jwtSection["Audience"] ?? "hamsterdocs-web";
        var expireMinutes = int.TryParse(jwtSection["ExpireMinutes"], out var em) ? em : 120;
        var secret = jwtSection["Secret"];
        if (string.IsNullOrWhiteSpace(secret))
        {
            secret = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
        }
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var claims = new List<Claim>
        {
            new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
            new Claim("role", user.Role ?? "User"),
            new Claim("displayName", user.UserName ?? user.Email ?? string.Empty),
            new Claim("avatar", user.Avatar ?? string.Empty)
        };

        var token = new JwtSecurityToken(
            issuer: issuer,
            audience: audience,
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(expireMinutes),
            signingCredentials: creds);

        var tokenString = new JwtSecurityTokenHandler().WriteToken(token);
        var defaultRoute = (user.Role ?? "User") == "Admin" ? "/admin" : "/panel";

        return new OkObjectResult(new LoginResponse(
            tokenString,
            expireMinutes * 60,
            new LoginUser(user.Id, user.UserName ?? string.Empty, user.Email ?? string.Empty, user.Role ?? "User", user.Avatar ?? string.Empty, defaultRoute)
        ));
    }

    public record LoginRequest(string Login, string? Password, string CaptchaId, string CaptchaCode);
    public record LoginUser(Guid Id, string UserName, string Email, string Role, string Avatar, string DefaultRoute);
    public record LoginResponse(string Token, int ExpiresIn, LoginUser User);
    public record MeResponse(Guid Id, string UserName, string Email, string Role, string Avatar, string DefaultRoute);
    public record OAuthExchangeRequest(string Code, string RedirectUri);
}